<!--
Page visualises *.csv files produced by CsvMetricsExporter -- i.e. OTel metrics exported in csv format

Uses 'jquery' for DOM manipulation, 'plotly.js' for plotting, and SumoSelect for nice <select> UI
(all under MIT license)

To add a new 'predefined' chart -- add apt function to a PLOTTERS array. Use already added functions
as an example
-->
<html>
<head>
  <meta charset="utf-8">
  <!-- license: MIT (https://github.com/jquery/jquery/blob/main/LICENSE.txt) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js" charset="utf-8"></script>

  <!-- license: MIT (https://github.com/plotly/plotly.js/blob/master/LICENSE) -->
  <script src="https://cdn.plot.ly/plotly-2.17.0.min.js" charset="utf-8"></script>

  <!-- Enhanced select with multi-option selection:
       (https://github.com/HemantNegi/jquery.sumoselect)
       (license: MIT) -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.sumoselect/3.4.9/jquery.sumoselect.min.js" charset="utf-8"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery.sumoselect/3.4.9/sumoselect.min.css">

  <style lang="css">
    body {
      font-family: Helvetica, serif;

      background-color: #F0F0F0;
      margin: 0;
      padding: 0;
    }

    /* ===== progress-bar details: =====*/

    #progressBar {
      display: block;
      background-color: rgba(200, 200, 200, 0.8);

      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;

      cursor: progress;

      z-index: 1000;
    }

    #progressBar > div {
      width: 30em;
      height: 5em;

      position: absolute;
      top: 40%;
      left: 50%;
      margin-left: -15em;
      margin-top: -2.5em;


      font-size: 2em;

      padding: 0.5em;
      text-align: center;

      border-radius: 5px;

      border: 2px outset darkgrey;

      background-color: lightgrey;
    }

    #progressBar > div > div {
      float: left;
      clear: both;
    }

    /* ===== file-chooser elements: =====*/

    .openFilesContainer {
      display: block;
    }

    .fileChooserForm {
      margin: 0;
    }

    /* Hide file chooser -- use it's <label> to trigger dialog */
    .fileInput {
      display: none;
    }

    #fileInputLabel {
      display: block;
      font-size: 1em;

      padding-top: 0.5em;

      background: #ccc;
      cursor: pointer;
      border-radius: 5px;
      border: 1px solid #ccc;
    }

    #loadedFilesInfo {
      display: block;

      padding: 0.5em;
      margin: 0.5em;
      font-size: 1.2em;

      background: #ccc;
      border-radius: 5px;
      border: 1px solid #ccc;
    }

    /* ========= starting screen:  ================== */

    div.splashScreen {
      background-color: rgba(200, 200, 200, 0.7);

      display: block;
      position: absolute;
      top: 0;
      left: 0;

      height: 100%;
      width: 100%;
      padding: 0;

      z-index: 100;

      font-size: 2em;
    }

    .splashScreen #fileChooserFormSplash {
      display: block;
      position: absolute;

      height: 3em;
      width: 40em;

      /*aka 'align-center hack' */
      top: 30%;
      left: 50%;
      margin-top: -1.5em;
      margin-left: -20em;
    }

    .splashScreen #fileInputLabelSplash {
      display: block;
      padding: 1em;

      text-align: center;

      border: 1px outset darkgrey;
      border-radius: 5px;

      font-size: 1.2em;

      background: #ccc;
      cursor: pointer;
    }

    .splashScreen .faq {
      margin-top: 2em;
      font-size: 0.7em;

      background: #ccc;
      border: 1px outset darkgrey;
      border-radius: 5px;
    }

    .splashScreen .faq dl {
      padding: 0.5em;
      margin: 0;
    }

    .splashScreen .faq dl dt::before {
      content: "Q: ";
      font-weight: bold;
    }

    .splashScreen .faq dl dd {
      margin-bottom: 0.5em;
      margin-inline-start: 0;
    }

    .splashScreen .faq dl dd::before {
      content: "A: ";
      font-weight: bold;
    }


    /* ========= plots:  ================== */

    div.blockOfPlots {
      border: 2px outset lightgrey;
      border-radius: 0.5em;

      margin: 0.5em;
    }

    div.caption {
      font-weight: bold;
      font-size: 1.2em;
      font-family: Helvetica, serif;

      padding: 0.4em;

      cursor: pointer;

      background-color: lightblue;


    }

    /* open/close markers */
    div > div.caption::before {
      content: '-';
      font-family: monospaced, monospace;
    }

    div.hidden > div.caption::before {
      content: '+';
      font-family: monospaced, monospace;
    }

    div.plot {
      /*border: 1px solid lightgrey;*/
      margin: 0;
    }

    div.hidden > div.hideable {
      display: none;
    }


    /* ========== SumoSelect customization ========= */

    .SumoSelect {
      width: 35em;
      font-size: 12pt;
    }
  </style>

  <!-- General objects/helpers: Point, TimeSeries, formatting methods -->
  <script lang="js">
    function Point(time, value) {
      this.time = time
      this.value = value
    }

    Point.prototype = {
      time: null,
      value: null,
      toString() {
        return this.time + ", " + this.value
      }
    }

    function TimeSeries(points) {
      this.points = points.sort((p1, p2) => {
        const t1 = p1.time.getTime()
        const t2 = p2.time.getTime()
        if (t1 > t2) {
          return 1
        }
        else if (t1 < t2) {
          return -1
        }
        else {
          return 0
        }
      })
      const pointByTime = {}
      for (const p of points) {
        pointByTime[p.time.getTime()] = p
      }
      this.pointByTime = pointByTime
    }

    TimeSeries.prototype = {
      points: null, //Array(Point{time, value})
      pointByTime: null, // Map{ time => Point(time, value) }
      toString() {
        return this.points.length + " points"
      },

      length() {
        return this.points.length
      },

      timestamps() {
        return this.points.map(p => p.time)
      },

      values() {
        return this.points.map(p => p.value)
      },

      combine(anotherTimeSeries, binaryOp) {
        const points = []
        this.points.forEach(p => {
          const ap = anotherTimeSeries.pointByTime[p.time.getTime()]
          if (ap) {
            points.push(new Point(p.time, binaryOp(p.value, ap.value)))
          }
          else {
            points.push(new Point(p.time, binaryOp(p.value, 0)))
          }
        })
        anotherTimeSeries.points.forEach(p => {
          const ap = this.pointByTime[p.time.getTime()]
          if (!ap) {
            points.push(new Point(p.time, binaryOp(0, p.value)))
          }
        })
        return new TimeSeries(points)
      },

      plus(anotherTimeSeries) {
        return this.combine(anotherTimeSeries, function (a, b) {
          return a + b
        })
      },
      minus(anotherTimeSeries) {
        return this.combine(anotherTimeSeries, function (a, b) {
          return a - b
        })
      },
      mul(anotherTimeSeries) {
        return this.combine(anotherTimeSeries, function (a, b) {
          return a * b
        })
      },
      div(anotherTimeSeries) {
        return this.combine(anotherTimeSeries, function (a, b) {
          return a / b
        })
      },
      mulScalar(scalar) {
        return new TimeSeries(this.points.map(p => new Point(p.time, p.value * scalar)))
      },
      cumSum() {
        let sum = 0
        return new TimeSeries(this.points.map(p => {
          sum += p.value
          return new Point(p.time, sum)
        }))
      }
    }

    Object.defineProperty(Number.prototype, 'formatSizeAsHumanReadable', {
      value: function () {
        const units = [' b', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB']
        const step = 1024
        let size = this
        let i = 0
        for (; i < units.length - 1; i++) {
          if (size < step) {
            break
          }
          else {
            size /= step
          }
        }
        return size.toFixed(1) + units[i]
      }
    })
  </script>

  <!-- Plotting: templates, plotting functions, predefined charts -->
  <!-- -->
  <script lang="js">
    document.loadedAndParsedData = {
      parsedCSV: null,        //parseFileContents() -> Array[ {name:String, startedAt:Date, value:Float} ]
      dateRange: {            //min/max of all (parsedCSV.startedAt)
        min: null,
        max: null
      },
      names: [],              //Array of {parsedCSV.name}
    }

    document.plots = []

    const PLOTLY_TEMPLATE = Plotly.makeTemplate({
      data: [{
        type: 'scatter',
        mode: 'lines+markers',
        connectgaps: true,
        line: {width: 1.5},
        marker: {size: 2},
      }],
      layout: {
        showlegend: true,
        dragmode: 'pan',
        legend: {x: 1, y: 1, xanchor: 'right'},
        margin: {l: 60, r: 20, t: 40, b: 40},
        xaxis: {type: 'date'},
        yaxis: {rangemode: 'tozero'}
      }
    })

    const PLOTLY_CONFIG = {
      displayModeBar: true,
      responsive: true,
      displaylogo: false,
      modeBarButtonsToRemove: ['select2d', 'lasso2d']
    }

    /* Extracts time series with name nameOfSeriesToPlot from loadedAndParsedData.
     * @return TimeSeries of Point(time: Date, value: value}
     */
    function extractTimeSeries(nameOfSeriesToPlot, parsedCSV = document.loadedAndParsedData.parsedCSV) {
      return new TimeSeries(
        parsedCSV
          .filter(row => row.name === nameOfSeriesToPlot)
          .map(row => new Point(row.startedAt, row.value))
      )
    }

    /* @param caption: String, plot title
     * @param domElementOrId container to plot into: id, DOM element, or jQuery object
     * @param dataToPlot: [{name, color, series, visible?}]
     * @param yaxis: optional {range, ...}
     */
    function plotTimeSeries(caption, domElementOrId, dataToPlot, yaxis) {
      let domElement
      if (domElementOrId instanceof jQuery) {
        domElement = domElementOrId[0]
      }
      else if (typeof (domElementOrId) == 'string') {
        domElement = $("#" + domElementOrId)[0]
      }
      else if (domElementOrId.id) {
        domElement = $(domElementOrId)[0]
      }
      else {
        throw `Unrecognized ${domElementOrId}: should be (ID | DOM element | jQuery object)`
      }
      console.log(`plotTimeSeries(${caption}, ${domElement.id}, ${dataToPlot.length} plots, ...)`)
      //RC: seems like Plotly purge previous plot by itself anyway
      // if (element.plot) {
      //     Plotly.purge(canvasElement);
      // }
      const plotlyTrace = dataToPlot.map(row => {
        const trace = {
          name: row.name,
          line: {color: row.color},
          marker: {color: row.color},
          x: row.series.timestamps(),
          y: row.series.values()
        }
        if (row.visible === 'legendonly') {
          trace.visible = 'legendonly'
        }
        return trace
      })

      const layout = {
        template: PLOTLY_TEMPLATE,
        title: caption,
        xaxis: {range: document.loadedAndParsedData.dateRange}
      }
      if (typeof (yaxis) !== 'undefined') {
        layout.yaxis = yaxis
      }
      const plot = Plotly.newPlot(
        domElement,
        plotlyTrace,
        layout,
        PLOTLY_CONFIG
      )
      //'synchronize' plots x-axes so that all plots show the same datetime range:
      domElement.on('plotly_relayout', (eventData) => {
        const from = eventData['xaxis.range[0]']
        const to = eventData['xaxis.range[1]']
        $(document.body).css('cursor', 'progress')
        try {
          if (from && to) {
            console.log(`${plot.id}: x-axis range: [${from}, ${to}]`)
            for (otherPlot of document.plots) {
              if (otherPlot !== plot) {
                console.log(`\treLayout: ${otherPlot.id}`)
                Plotly.relayout(otherPlot, {'xaxis.range': [from, to]})
              }
            }
          }
        }
        finally {
          $(document.body).css('cursor', 'default')
        }
      })

      domElement.plot = plot
      if (!document.plots.includes(domElement)) {
        document.plots.push(domElement)
      }

      return domElement
    }

    //==== Plots for specific subsystem, tailored and customized: ============

    function plotBasicJVMCharts() {
      if (!document.loadedAndParsedData.parsedCSV) {
        console.log("Error: .loadedAndParsedData is not loaded")
        return
      }

      const usedHeapBytes = extractTimeSeries("JVM.usedHeapBytes")
      const maxHeapBytes = extractTimeSeries("JVM.maxHeapBytes")
      const usedNativeBytes = extractTimeSeries("JVM.usedNativeBytes")
      const maxNativeBytes = extractTimeSeries("JVM.maxNativeBytes")
      const totalDirectByteBuffersBytes = extractTimeSeries("JVM.totalDirectByteBuffersBytes")
      const threadsCount = extractTimeSeries("JVM.threadCount")
      const maxThreadsCount = extractTimeSeries("JVM.maxThreadCount")
      const newThreadsCount = extractTimeSeries("JVM.newThreadsCount")

      const gcCollections = extractTimeSeries("JVM.GC.collections")
      const gcTimesMs = extractTimeSeries("JVM.GC.collectionTimesMs")
      const totalBytesAllocated = extractTimeSeries("JVM.totalBytesAllocated")

      const totalCpuTimesMs = extractTimeSeries("JVM.totalCpuTimeMs")
      const osLoadAverage = extractTimeSeries("OS.loadAverage")

      console.log("JVM events: " + usedHeapBytes.length())


      plotTimeSeries(
        'Heap memory use',
        'jvmBasics_Heap_Chart',
        [{
          name: 'Used heap, Mb',
          color: 'green',
          series: usedHeapBytes.mulScalar(1.0 / 1024 / 1024)
        }, {
          name: 'Max heap, Mb',
          color: 'red',
          series: maxHeapBytes.mulScalar(1.0 / 1024 / 1024)
        }],
        /*yaxis:*/ {
          title: 'Mb'
        }
      )

      const nativeMemoryNotLimited = maxNativeBytes.values().every(value => value <= 0)
      if (nativeMemoryNotLimited) {
        plotTimeSeries(
          'Off-Heap (native) memory use',
          'jvmBasics_OffHeap_Chart',
          [{
            name: 'Used native, Mb',
            color: 'green',
            series: usedNativeBytes.mulScalar(1.0 / 1024 / 1024)
          }],
          /*yaxis:*/ {
            title: 'Mb'
          }
        )
      }
      else {
        plotTimeSeries(
          'Off-Heap (native) memory use',
          'jvmBasics_OffHeap_Chart',
          [{
            name: 'Used native, Mb',
            color: 'green',
            series: usedNativeBytes.mulScalar(1.0 / 1024 / 1024)
          }, {
            name: 'Max native, Mb',
            color: 'red',
            series: maxNativeBytes.mulScalar(1.0 / 1024 / 1024)
          }],
          /*yaxis:*/ {
            title: 'Mb'
          }
        )
      }

      plotTimeSeries(
        'Direct ByteBuffers',
        'jvmBasics_DirectByteBuffers_Chart',
        [{
          name: 'Total DirectByteBuffers, Mb',
          color: 'green',
          series: totalDirectByteBuffersBytes.mulScalar(1.0 / 1024 / 1024)
        }],
        /*yaxis:*/ {
          title: 'Mb'
        }
      )


      plotTimeSeries(
        'Threads count',
        'jvmBasics_Threads_Chart',
        [{
          name: 'current threads count',
          color: 'blue',
          series: threadsCount
        }, {
          name: 'max threads count',
          color: 'red',
          series: maxThreadsCount
        }, {
          name: 'new threads started',
          color: 'orange',
          series: newThreadsCount
        }],
        /*yaxis:*/ {
          title: 'threads'
        }
      )

      plotTimeSeries(
        'GC times',
        'jvmBasics_GC_Times_Chart',
        [{
          name: 'GC collections time, ms',
          color: 'blue',
          series: gcTimesMs
        }],
        /*yaxis:*/ {
          title: 'ms'
        }
      )

      plotTimeSeries(
        'Allocations',
        'jvmBasics_Allocations_Chart',
        [{
          name: 'Allocations',
          color: 'red',
          series: totalBytesAllocated.mulScalar(1.0/1024/1024/1024)
        }],
        /*yaxis:*/ {
          title: 'Gb'
        }
      )

      plotTimeSeries(
        'OS load average',
        'jvmBasics_OS_LoadAvg_Chart',
        [{
          name: 'OS load average, %',
          color: 'blue',
          series: osLoadAverage.mulScalar(100)
        }],
        /*yaxis:*/ {
          title: '%'
        }
      )

      plotTimeSeries(
        'JVM CPU Time',
        'jvmBasics_JVM_CPUTime_Chart',
        [{
          name: 'JVM CPU time, sec',
          color: 'blue',
          series: totalCpuTimesMs.mulScalar(1.0 / 1000)
        }],
        /*yaxis:*/ {
          title: 'sec'
        }
      )
    }

    function plotAWTQueueCharts() {
      if (!document.loadedAndParsedData.parsedCSV) {
        console.log("Error: .loadedAndParsedData is not loaded")
        return
      }

      const awtEventsCount = extractTimeSeries("AWTEventQueue.eventsDispatched")
      const awtDispatchTimeMaxMs = extractTimeSeries("AWTEventQueue.dispatchTimeMaxNs").mulScalar(1e-6 /* ns-> ms */)
      const awtDispatchTime90PMs = extractTimeSeries("AWTEventQueue.dispatchTime90PNs").mulScalar(1e-6 /* ns-> ms */)
      const awtDispatchTimeAvgMs = extractTimeSeries("AWTEventQueue.dispatchTimeAvgNs").mulScalar(1e-6 /* ns-> ms */)

      console.log("AWT events: " + awtEventsCount)
      console.log("AWT dispatch time avg: " + awtDispatchTimeAvgMs)

      plotTimeSeries(
        'AWT event queue: events count',
        'EDT_awtEventsDispatchedChart',
        [{
          name: 'events dispatched',
          color: 'blue',
          series: awtEventsCount
        }],
        /*yaxis:*/ {
          title: 'events'
        }
      )

      plotTimeSeries(
        'AWT event queue: event dispatching times',
        'EDT_awtEventsTimingsChart',
        [{
          name: 'avg, ms',
          color: 'green',
          series: awtDispatchTimeAvgMs
        }, {
          name: '90%, ms',
          color: 'orange',
          series: awtDispatchTime90PMs
        }, {
          name: 'MAX, ms',
          color: 'red',
          series: awtDispatchTimeMaxMs,
          visible: 'legendonly'  //MAX is too dominating => toggle off by default
        }],
        /*yaxis:*/ {
          title: 'ms'
        }
      )
    }

    function plotFlushQueueCharts() {
      if (!document.loadedAndParsedData.parsedCSV) {
        console.log("Error: .loadedAndParsedData is not loaded")
        return
      }

      //FlushQueue: ========
      const flushQueueTasksExecuted = extractTimeSeries("FlushQueue.tasksExecuted")

      const flushQueueSizeAvg = extractTimeSeries("FlushQueue.queueSizeAvg")
      const flushQueueSizeMax = extractTimeSeries("FlushQueue.queueSizeMax")
      const flushQueueSize90P = extractTimeSeries("FlushQueue.queueSize90P")

      const flushQueueWaitingTimeMaxMs = extractTimeSeries("FlushQueue.waitingTimeMaxNs").mulScalar(1e-6)
      const flushQueueWaitingTime90PMs = extractTimeSeries("FlushQueue.waitingTime90PNs").mulScalar(1e-6)
      const flushQueueWaitingTimeAvgMs = extractTimeSeries("FlushQueue.waitingTimeAvgNs").mulScalar(1e-6)

      const flushQueueExecutionTimeMaxMs = extractTimeSeries("FlushQueue.executionTimeMaxNs").mulScalar(1e-6)
      const flushQueueExecutionTime90PMs = extractTimeSeries("FlushQueue.executionTime90PNs").mulScalar(1e-6)
      const flushQueueExecutionTimeAvgMs = extractTimeSeries("FlushQueue.executionTimeAvgNs").mulScalar(1e-6)

      plotTimeSeries(
        'FlushQueue: events count',
        'FlushQueue_tasksExecutedChart',
        [{
          name: 'events dispatched',
          color: 'blue',
          series: flushQueueTasksExecuted
        }],
        /*yaxis:*/ {
          title: 'events',
          autorange: true
        }
      )

      plotTimeSeries(
        'FlushQueue: waiting times (ms)',
        'FlushQueue_tasksWaitingTimesChart',
        [{
          name: 'avg, ms',
          color: 'green',
          series: flushQueueWaitingTimeAvgMs
        }, {
          name: '90%, ms',
          color: 'orange',
          series: flushQueueWaitingTime90PMs
        }, {
          name: 'MAX, ms',
          color: 'red',
          series: flushQueueWaitingTimeMaxMs,
          visible: 'legendonly' //MAX is too dominating => toggle off by default
        }],
        /*yaxis:*/ {
          title: 'ms'
        }
      )

      plotTimeSeries(
        'FlushQueue: execution times (ms)',
        'FlushQueue_tasksExecutionTimesChart',
        [{
          name: 'avg, ms',
          color: 'green',
          series: flushQueueExecutionTimeAvgMs
        }, {
          name: '90%, ms',
          color: 'orange',
          series: flushQueueExecutionTime90PMs
        }, {
          name: 'MAX, ms',
          color: 'red',
          series: flushQueueExecutionTimeMaxMs,
          visible: 'legendonly'  //MAX is too dominating => toggle them off by default
        }],
        /*yaxis:*/ {
          title: 'ms'
        }
      )


    }

    function plotReadWriteActionsChart() {
      if (!document.loadedAndParsedData.parsedCSV) {
        console.log("Error: .loadedAndParsedData is not loaded")
        return
      }

      const writeActionExecutionsCount = extractTimeSeries("WriteAction.executionsCount")
      const readActionExecutionsCount = extractTimeSeries("ReadAction.executionsCount")

      const finalizedExecutionsCount = extractTimeSeries("NonBlockingReadAction.finalizedExecutionsCount")
      const failedExecutionsCount = extractTimeSeries("NonBlockingReadAction.failedExecutionsCount")
      const finalizedExecutionTimeMs = extractTimeSeries("NonBlockingReadAction.finalizedExecutionTimeUs").mulScalar(1e-3)
      const failedExecutionTimeMs = extractTimeSeries("NonBlockingReadAction.failedExecutionTimeUs").mulScalar(1e-3)

      plotTimeSeries(
        'Read and Write Actions count',
        'ReadAndWriteActions_CountChart',
        [{
          name: 'Write Actions',
          color: 'red',
          series: writeActionExecutionsCount
        }, {
          name: 'Read Actions',
          color: 'blue',
          series: readActionExecutionsCount
        }],
        /*yaxis:*/ {
          title: 'actions'
        }
      )

      plotTimeSeries(
        'NonBlockingReadActions count: successful/interrupted',
        'NonBlockingReads_CountChart',
        [{
          name: 'successful',
          color: 'green',
          series: finalizedExecutionsCount
        }, {
          name: 'failed/interrupted',
          color: 'red',
          series: failedExecutionsCount
        }],
        /*yaxis:*/ {
          title: 'events'
        }
      )

      plotTimeSeries(
        'NonBlockingReadActions times: useful/wasted',
        'NonBlockingReads_TimesChart',
        [{
          name: 'useful (succeeded) time, ms',
          color: 'green',
          series: finalizedExecutionTimeMs
        }, {
          name: 'wasted (interrupted) time, ms',
          color: 'red',
          series: failedExecutionTimeMs
        }],
        /*yaxis:*/ {
          title: 'ms'
        }
      )
    }

    function plotIndexesCharts() {
      if (!document.loadedAndParsedData.parsedCSV) {
        console.log("Error: .loadedAndParsedData is not loaded")
        return
      }

      const allKeysLookups = extractTimeSeries("Indexes.allKeys.lookups")
      const allKeysLookupsAvgMs = extractTimeSeries("Indexes.allKeys.lookupDurationAvgMs")
      const allKeysLookups90PMs = extractTimeSeries("Indexes.allKeys.lookupDuration90PMs")
      const allKeysLookupsMaxMs = extractTimeSeries("Indexes.allKeys.lookupDurationMaxMs")

      const stubIndexLookups = extractTimeSeries("Indexes.stubs.lookups")
      const stubIndexLookupsAvgMs = extractTimeSeries("Indexes.stubs.lookupDurationAvgMs")
      const stubIndexLookups90PMs = extractTimeSeries("Indexes.stubs.lookupDuration90PMs")
      const stubIndexLookupsMaxMs = extractTimeSeries("Indexes.stubs.lookupDurationMaxMs")

      const entriesIndexLookups = extractTimeSeries("Indexes.entries.lookups")
      const entriesIndexLookupsAvgMs = extractTimeSeries("Indexes.entries.lookupDurationAvgMs")
      const entriesIndexLookups90PMs = extractTimeSeries("Indexes.entries.lookupDuration90PMs")
      const entriesIndexLookupsMaxMs = extractTimeSeries("Indexes.entries.lookupDurationMaxMs")

      console.log("Indexes events: " + stubIndexLookups.length())

      const totalTimeSpentInIndexLookupsMs = allKeysLookups.mul(allKeysLookupsAvgMs)
        .plus(stubIndexLookups.mul(stubIndexLookupsAvgMs))
        .plus(entriesIndexLookups.mul(entriesIndexLookupsAvgMs))

      const maxIndexLookupDurationMs = allKeysLookupsMaxMs
        .combine(stubIndexLookupsMaxMs, Math.max)
        .combine(entriesIndexLookupsMaxMs, Math.max)

      plotTimeSeries(
        'Indexes lookups count',
        'indexes_Lookups_Chart',
        [{
          name: 'Stub lookups',
          color: 'green',
          series: stubIndexLookups
        }, {
          name: 'Entries lookups',
          color: 'orange',
          series: entriesIndexLookups
        }, {
          name: 'All-keys lookups',
          color: 'blue',
          series: allKeysLookups
        }],
        /*yaxis:*/ {
          title: 'count'
        }
      )

      plotTimeSeries(
        'Indexes lookup duration: total(stub+entries+allKeys), and MAX(stub,entries,allKeys)',
        'indexes_Durations_Chart',
        [{
          name: 'total time spent, sec',
          color: 'green',
          series: totalTimeSpentInIndexLookupsMs.mulScalar(1 / 1000) //ms->sec
        }, {
          name: 'MAX, sec',
          color: 'red',
          series: maxIndexLookupDurationMs.mulScalar(1 / 1000) //ms->sec
        }],
        /*yaxis:*/ {
          title: 'sec'
        }
      )
    }

    function plotFilePageCacheCharts() {
      if (!document.loadedAndParsedData.parsedCSV) {
        console.log("Error: .loadedAndParsedData is not loaded")
        return
      }
      const pageFastHits = extractTimeSeries("FilePageCache.pageFastCacheHits")
      const pageHits = extractTimeSeries("FilePageCache.pageHits")
      const pageLoads = extractTimeSeries("FilePageCache.pageLoads")
      const pageMisses = extractTimeSeries("FilePageCache.pageLoadsAboveSizeThreshold")

      const pageLoadsTimeUs = extractTimeSeries("FilePageCache.totalPageLoadsUs")
      const pageDisposalTimeUs = extractTimeSeries("FilePageCache.totalPageDisposalsUs")

      const bufferCacheHits = extractTimeSeries("DirectByteBufferAllocator.hits")
      const bufferCacheMisses = extractTimeSeries("DirectByteBufferAllocator.misses")
      const buffersReclaimed = extractTimeSeries("DirectByteBufferAllocator.reclaimed")
      const buffersDisposed = extractTimeSeries("DirectByteBufferAllocator.disposed")
      const totalSizeOfBuffersInCacheBytes = extractTimeSeries("DirectByteBufferAllocator.totalSizeOfBuffersCachedInBytes")
      const totalSizeOfBuffersAllocatedBytes = extractTimeSeries("DirectByteBufferAllocator.totalSizeOfBuffersAllocatedInBytes")


      console.log("FPC fast hits: " + pageFastHits)

      const totalPagesRequested = pageFastHits.plus(pageHits).plus(pageMisses).plus(pageLoads)
      const pageFastHitsPercent = pageFastHits.div(totalPagesRequested).mulScalar(100)
      const pageHitsPercent = pageHits.div(totalPagesRequested).mulScalar(100)
      const pageLoadsPercent = pageLoads.plus(pageMisses).div(totalPagesRequested).mulScalar(100)

      plotTimeSeries(
        'FilePageCache: hits/misses/loads',
        $("#filePageCache_HitsMisses_Chart"),
        [
          {name: 'Fast hits', color: 'green', series: pageFastHitsPercent},
          {name: 'Regular hits', color: 'blue', series: pageHitsPercent},
          {name: 'Misses (loads)', color: 'red', series: pageLoadsPercent},
        ],
        /*yaxis: */ {
          title: '%',
          autorange: false,
          range: [0, 100]
        }
      )

      plotTimeSeries(
        'FilePageCache: loads/dispose times',
        $("#filePageCache_Times_Chart"),
        [
          {name: 'Page load times, ms', color: 'green', series: pageLoadsTimeUs.mulScalar(1e-3 /*us->ms*/)},
          {name: 'Page dispose times, ms', color: 'blue', series: pageDisposalTimeUs.mulScalar(1e-3 /*us->ms*/)}
        ],
        /*yaxis: */ {
          title: 'ms',
          autorange: true
        }
      )

      //DirectBufferAllocator charts:

      plotTimeSeries(
        'DirectBufferAllocator: caching stats',
        $("#directBufferAllocator_Counts_Chart"),
        [
          {name: 'Cache hits', color: 'green', series: bufferCacheHits},
          {name: 'Cache misses', color: 'red', series: bufferCacheMisses},
          //RC: buffers disposed & re-used are quite niche -- turn them off by default:
          {name: 'Buffers disposed', color: 'yellow', series: buffersDisposed, visible: 'legendonly'},
          {name: 'Buffers re-used', color: 'blue', series: buffersReclaimed, visible: 'legendonly'},
        ],
        /*yaxis: */ {
          title: '',
          autorange: true
        }
      )

      plotTimeSeries(
        'DirectBufferAllocator: native memory usage',
        $("#directBufferAllocator_Bytes_Chart"),
        [
          {name: 'ByteBuffers allocated, Mb', color: 'red', series: totalSizeOfBuffersAllocatedBytes.mulScalar(1 / 1024 / 1024)},
          {name: 'ByteBuffers in cache, Mb', color: 'green', series: totalSizeOfBuffersInCacheBytes.mulScalar(1 / 1024 / 1024)}
        ],
        /*yaxis: */ {
          title: 'Mb',
          autorange: true
        }
      )
    }

    function plotFilePageCacheLockFreeCharts() {
      if (!document.loadedAndParsedData.parsedCSV) {
        console.log("Error: .loadedAndParsedData is not loaded")
        return
      }
      const totalNativeBytesAllocated = extractTimeSeries("FilePageCacheLockFree.totalNativeBytesAllocated")
      const totalNativeBytesReclaimed = extractTimeSeries("FilePageCacheLockFree.totalNativeBytesReclaimed")
      const totalHeapBytesAllocated = extractTimeSeries("FilePageCacheLockFree.totalHeapBytesAllocated")
      const totalHeapBytesReclaimed = extractTimeSeries("FilePageCacheLockFree.totalHeapBytesReclaimed")

      const nativeBytesInUse = extractTimeSeries("FilePageCacheLockFree.nativeBytesInUse")
      const heapBytesInUse = extractTimeSeries("FilePageCacheLockFree.heapBytesInUse")

      const totalPagesAllocated = extractTimeSeries("FilePageCacheLockFree.totalPagesAllocated")
      const totalPagesReclaimed = extractTimeSeries("FilePageCacheLockFree.totalPagesReclaimed")
      const totalPagesHandedOver = extractTimeSeries("FilePageCacheLockFree.totalPagesHandedOver")
      const totalPageAllocationsWaited = extractTimeSeries("FilePageCacheLockFree.totalPageAllocationsWaited")

      const totalPagesWritten = extractTimeSeries("FilePageCacheLockFree.totalPagesWritten")
      const totalPagesRequested = extractTimeSeries("FilePageCacheLockFree.totalPagesRequested")

      const totalBytesRequested = extractTimeSeries("FilePageCacheLockFree.totalBytesRequested")
      const totalBytesRead = extractTimeSeries("FilePageCacheLockFree.totalBytesRead")
      const totalBytesWritten = extractTimeSeries("FilePageCacheLockFree.totalBytesWritten")

      const totalPagesRequestsMs = extractTimeSeries("FilePageCacheLockFree.totalPagesRequestsMs")
      const totalPagesReadMs = extractTimeSeries("FilePageCacheLockFree.totalPagesReadMs")
      const totalPagesWriteMs = extractTimeSeries("FilePageCacheLockFree.totalPagesWriteMs")

      const housekeeperTurnsDone = extractTimeSeries("FilePageCacheLockFree.housekeeperTurnsDone")
      const housekeeperTurnsSkipped = extractTimeSeries("FilePageCacheLockFree.housekeeperTurnsSkipped")
      const housekeeperTimeSpentMs = extractTimeSeries("FilePageCacheLockFree.housekeeperTimeSpentMs")

      const totalClosedStoragesReclaimed = extractTimeSeries("FilePageCacheLockFree.totalClosedStoragesReclaimed")

      console.log("FPC native bytes allocated: " + totalNativeBytesAllocated)

      // const pageLoadsPercent = pageLoads.plus(pageMisses).div(totalPagesRequested).mulScalar(100)

      plotTimeSeries(
        'Page count: requested, allocated, reclaimed...',
        $("#filePageCacheLockFree_PageCounts"),
        [
          {name: 'Pages requested by app', color: 'green', series: totalPagesRequested, visible: 'legendonly'},
          {name: 'Pages allocated', color: 'red', series: totalPagesAllocated},
          {name: 'Pages reclaimed', color: 'blue', series: totalPagesReclaimed},
          {name: 'Pages reused immediately', color: 'cyan', series: totalPagesHandedOver},
          {name: 'Pages written on disk', color: 'magenta', series: totalPagesWritten},
          {name: 'Pages allocation waited', color: 'orange', series: totalPageAllocationsWaited}
        ],
        /*yaxis: */ {
          title: '',
          autorange: true
        }
      )

      plotTimeSeries(
        'Page timings',
        $("#filePageCacheLockFree_PageTimings"),
        [
          {name: 'Total time of page requests', color: 'blue', series: totalPagesRequestsMs.mulScalar(1e-3)},
          {name: 'Total time of page reads', color: 'green', series: totalPagesReadMs.mulScalar(1e-3)},
          {name: 'Total time of page writes', color: 'red', series: totalPagesWriteMs.mulScalar(1e-3)}
        ],
        /*yaxis: */ {
          title: 'sec',
          autorange: true
        }
      )

      plotTimeSeries(
        'Data flows',
        $("#filePageCacheLockFree_PageBytes"),
        [
          {
            name: 'Total bytes requested by app', color: 'blue', series: totalBytesRequested.mulScalar(1e-6),
            visible: 'legendonly'
          },//usually not representative
          {name: 'Total bytes read', color: 'green', series: totalBytesRead.mulScalar(1e-6)},
          {name: 'Total bytes written', color: 'red', series: totalBytesWritten.mulScalar(1e-6)}
        ],
        /*yaxis: */ {
          title: 'Mb',
          autorange: true
        }
      )

      plotTimeSeries(
        'Caching efficiency',
        $("#filePageCacheLockFree_Caching"),
        [
          {name: 'Cache hits', color: 'green',
            series: totalPagesRequested.minus(totalPagesAllocated).div(totalPagesRequested).mulScalar(100)
          },
          {name: 'Cache misses', color: 'red',
            series: totalPagesAllocated.div(totalPagesRequested).mulScalar(100)
          }
        ],
        /*yaxis: */ {
          title: '%',
          autorange: false,
          range: [0, 100]
        }
      )

      //MAYBE RC: read/write _speeds_ feel to be more representative (i.e. to detect slow storage). But
      //          in practice numbers are quite irregular -- likely because since OS-level caching plays
      //          too big role.
      // plotTimeSeries(
      //   'Data flows',
      //   $("#filePageCacheLockFree_PageBytes"),
      //   [
      //     {name: 'Requested by app', color: 'blue',
      //       series: totalBytesRequested.mulScalar(1e-6).div(totalPagesRequestsMs.mulScalar(1e-3)),
      //       visible: 'legendonly'},//usually not representative
      //     {name: 'Read', color: 'green', series: totalBytesRead.mulScalar(1e-6).div(totalPagesReadMs.mulScalar(1e-3))},
      //     {name: 'Written', color: 'red', series: totalBytesWritten.mulScalar(1e-6).div(totalPagesWriteMs.mulScalar(1e-3))}
      //   ],
      //   /*yaxis: */ {
      //     title: 'Mb/sec',
      //     autorange: true
      //   }
      // )

      plotTimeSeries(
        'Memory used by page buffers: Heap vs Off-heap (Native)',
        $("#filePageCacheLockFree_HeapVsNativeMemoryUsed"),
        [
          {name: 'Off-heap memory in use', color: 'blue', series: nativeBytesInUse.mulScalar(1e-6)},
          {name: 'Heap memory in use', color: 'red', series: heapBytesInUse.mulScalar(1e-6)}
        ],
        /*yaxis: */ {
          title: 'Mb',
          autorange: true
        }
      )

      plotTimeSeries(
        'Housekeeper thread activity',
        $("#filePageCacheLockFree_Housekeeper"),
        [
          {name: 'Turns done', color: 'green', series: housekeeperTurnsDone},
          {name: 'Turns skipped', color: 'blue', series: housekeeperTurnsSkipped, visible: 'legendonly'}
        ],
        /*yaxis: */ {
          title: '',
          autorange: true
        }
      )

      plotTimeSeries(
        'Housekeeper thread times',
        $("#filePageCacheLockFree_HousekeeperTimes"),
        [
          {name: 'Time spent', color: 'green', series: housekeeperTimeSpentMs.mulScalar(1e-3)}
        ],
        /*yaxis: */ {
          title: 'sec',
          autorange: true
        }
      )
    }

    //====== 'Custom' plot: =================

    /** namesOfSeriesToPlot: array of strings, names of time series in a document.loadedAndParsedData.parsedCSV */
    function extractAndPlotCustomTimeSeries(namesOfSeriesToPlot) {
      if (!document.loadedAndParsedData.parsedCSV) {
        console.log("Error: .loadedAndParsedData is not loaded")
        return
      }
      const canvasElementToPlotOn = $("#customChartPlotly")
      if (!Array.isArray(namesOfSeriesToPlot)) { //legacy version: not an array, just a string
        namesOfSeriesToPlot = [namesOfSeriesToPlot]
      }
      const title = namesOfSeriesToPlot.join(', ')
      const dataToPlot = namesOfSeriesToPlot.map((timeSeriesName) => {
        const timeSeries = extractTimeSeries(timeSeriesName)
        if (!timeSeries) {
          console.log(`Error: .loadedAndParsedData.parsedCSV[${timeSeriesName}] is not exists`)
          console.log(document.loadedAndParsedData.parsedCSV)
        }
        return {
          name: timeSeriesName,
          series: timeSeries
        }
      }).filter((dataRow) => {
        return dataRow.series != null
      })

      plotTimeSeries(
        title,
        canvasElementToPlotOn,
        dataToPlot
      )
    }

    function plotCustomTimeSeries(title, timeSeries, canvasElement) {
      plotTimeSeries(
        title,
        canvasElement,
        [{
          name: title,
          color: 'green',
          series: timeSeries
        }]
      )
    }


    //================= List of all 'plotter' functions:      =================
    // Append newly created plotters here, to be automatically caught on file loading:
    const PLOTTERS = [
      plotBasicJVMCharts,
      plotAWTQueueCharts,
      plotFlushQueueCharts,
      plotReadWriteActionsChart,
      plotIndexesCharts,
      plotFilePageCacheCharts,
      plotFilePageCacheLockFreeCharts
    ]
  </script>

  <!-- File(s) loading and binding all together: -->
  <script lang="js">
    function readFiles(files) {
      console.log("Files: " + files.length)
      if (files.length === 0) {
        return
      }

      $("#splashScreen").hide()
      document.progressBar.show(`Parsing ${files.length} files...`)
      document.progressBar.update(0)

      for (plot of document.plots) {
        Plotly.purge(plot)
      }
      document.plots = []

      const fileNames = Array.from(files)
        .reduce((string, file) => `${string + file.name} (${file.size.formatSizeAsHumanReadable()}) `, "")
      const totalFileSize = Array.from(files)
        .map(file => file.size)
        .reduce((total, size) => total + size, 0)

      const loadingChain = new Promise((resolve, reject) => {
        let fileContents = []
        for (file of files) {
          const fileName = file.name
          const reader = new FileReader()
          reader.addEventListener('load', (event) => {
            const fileText = event.target.result
            console.log(`\tread ${fileName}: ${fileText.length} b`)

            fileContents.push(fileText)
            if (fileContents.length < files.length) {
              document.progressBar.update(fileContents.length * 40 / files.length)
            }
            else {
              //all files done:
              resolve(fileContents)
            }
          })
          console.log(`reading ${fileName}...`)
          reader.readAsText(file)
        }
      })
        .then(parseFileContents)
        .then(csvRows => {
          document.progressBar.update(69)

          document.loadedAndParsedData.parsedCSV = csvRows
          document.loadedAndParsedData.names = [...new Set(csvRows.map(row => {
            return row.name
          }))]

          let minTs = 1e30, maxTs = 0
          csvRows.map(row => {
            return row.startedAt.getTime()
          }).forEach(timestamp => {
            minTs = Math.min(minTs, timestamp)
            maxTs = Math.max(maxTs, timestamp)
          })
          document.loadedAndParsedData.dateRange = [new Date(minTs), new Date(maxTs)]

          $("#filesLoadedInfo")
            .html(`Loaded <b>${files.length} file(s)</b>: ${totalFileSize.formatSizeAsHumanReadable()}, <b>${csvRows.length}</b> points`)
            .attr('title', fileNames)
          $("#pointsLoadedInfo").html(
            `Interval covered: [${new Date(minTs).toLocaleString()}  &mdash;  ${new Date(maxTs).toLocaleString()}] (local TZ)`
          )
          document.progressBar.show(`Parsed ${files.length} files, ${totalFileSize.formatSizeAsHumanReadable()}, plotting...`)
          document.progressBar.update(74)
          return csvRows
        })
        .then(updateUIAfterDataLoaded)


      //Plot charts:
      const progressPerPlotter = (100 - 80 - 1) / PLOTTERS.length
      loadingChain.then(() => {
        document.progressBar.update(80)
      })
      for (const plotter of PLOTTERS) {
        loadingChain
          .then(plotter)
          .then(() => {
            document.progressBar.update(document.progressBar.currentValue() + progressPerPlotter)
          })
      }
      loadingChain.then(() => {
        document.progressBar.update(80)
        document.progressBar.hide()
      })
    }

    /* @param contents: String, multi-lines csv
     * @return Array of records {name, startedAt:Date, value: float}
     */
    function parseFileContents(contents) {
      console.log("File contents: " + contents.length)
      let data = []
      for (const content of contents) {
        const lines = content.split('\n')
        for (const line of lines) {
          if (!line.startsWith('#') && line.trim().length > 0) {
            const parts = line.split(',')
            if (parts.length === 4) {//name, startEpochNs, endEpochNs, value:
              data.push({
                name: parts[0].trim(),
                startedAt: new Date(parseInt(parts[1].trim()) / 1_000_000 /* ns -> ms */),
                value: parseFloat(parts[3].trim())
              })
            }
            else {
              console.log("Error parsing line: [" + line + "]")
            }
          }
        }
      }

      return data
    }

    /* Updates UI after CSV data is loaded */
    function updateUIAfterDataLoaded(data) {
      const names = document.loadedAndParsedData.names

      //Enhance default html <select multi> with nice UI:
      const timeSeriesChooser = $("#timeSeriesChooser")
      const previouslySelectedValues = timeSeriesChooser.val()
      timeSeriesChooser.html("")
      for (const name of names.sort()) {
        timeSeriesChooser.append(`<option value="${name}">${name}</option>`)
      }

      if (timeSeriesChooser[0].sumo) {
        timeSeriesChooser[0].sumo.reload()
        for (const valueToSelect of previouslySelectedValues) {
          timeSeriesChooser[0].sumo.selectItem(valueToSelect)
        }
      }
      else {
        timeSeriesChooser.SumoSelect({
          placeholder: 'Choose time series to plot...',
          max: 6,
          csvDispCount: 6,

          search: true

          // clearAll: true -- has some issues
        })
        timeSeriesChooser[0].sumo.selectItem(0)
      }


      //TODO RC: Unfinished work: setup _time-range_ chooser -- so user could limit plots to subset
      //         of datetime range covered in files.
      //         (For now #timeRangeChooser is hidden to not distract users)
      const timeRangeChooser = $("#timeRangeChooser")
      timeRangeChooser.append(`<option value="">---all---</option>`)

      const min = document.loadedAndParsedData.dateRange[0]
      const max = document.loadedAndParsedData.dateRange[1]
      //RC how to iterate time range hour by hour?
      // for(v=min; v<max; v++) {
      //     $timeRangeChooser.append(`<option value="${v}">${v}</option>`)
      // }
    }
  </script>
  <title>OTel.Metrics Plotter (*.csv)</title>
</head>
<body>

<div id="progressBar" style="display: none">
  <div>
    <div id="caption">LOADING...</div>
    <div><span id="percents">0</span>%</div>
    <div>(Please be patient: browser is working hard for your honor!)</div>
  </div>
</div>

<div id="splashScreen" class="splashScreen">
  <form id="fileChooserFormSplash">
    <label id="fileInputLabelSplash" for="fileInputSplash" autofocus class="fileInputLabel">
      Drag & Drop <span style="font-family: monospace">open-telemetry-metrics.*.csv</span> file(s) on the page<br/>
      (or click here for file-open dialog)
    </label>

    <input type="file"
           id="fileInputSplash" multiple
           accept="text/csv"
           title="Select 'open-telemetry-metrics.*.csv' files"
           class="fileInput"
           onchange="readFiles(event.target.files)"
    />

    <div class="faq hidden">
      <div class="caption">FAQ (What is this?)</div>
      <div class="hideable">
        <dl>
          <dt>What this page is for?</dt>
          <dd>During its work IDE exports its internal monitoring data into a <span style="font-family: monospace">open-telemetry-metrics.*.csv</span>
            files. The page could parse those files and plot nice time series charts. The data is mostly for JetBrains support and
            development engineers.
          </dd>

          <dt>Why do I need it?</dt>
          <dd>Maybe you don't need it. But it is quite easy to try and see yourself: drag-n-drop
            any <span style="font-family: monospace">open-telemetry-metrics.*.csv</span>
            file onto the page.
          </dd>

          <dt>There to find <span style="font-family: monospace">open-telemetry-metrics.*.csv</span>
            files?
          </dt>
          <dd>In IDE logs folder (menu: <span style="font-family: monospace">Help/Show logs in Finder</span>)</dd>
        </dl>
      </div>
    </div>
  </form>

</div>

<div id="loadedFilesInfo">
  <div id="filesLoadedInfo"></div>
  <div id="pointsLoadedInfo"></div>

  <div class="openFilesContainer">
    <form id="fileChooserForm" class="fileChooserForm">

      <label for="timeRangeChooser" style="display: none">Reduce time range to specific hour:</label>
      <select id="timeRangeChooser" style="display: none"></select>

      <!--            class="fileInputLabel"-->
      <label id="fileInputLabel" for="fileInput" autofocus>
        To view another file(s): <b>Drag & Drop</b> <span style="font-family: monospace">*.csv</span> file(s) on the page,
        or <b>click</b> here to use file-choosing dialog.
      </label>
      <input type="file"
             id="fileInput"
             accept="text/csv" multiple
             class="fileInput"
             title="Select 'open-telemetry-metrics.*.csv' files"
             onchange="readFiles(event.target.files)"
      />

    </form>
  </div>
</div>

<div id="plots">
  <div id="jvmBasics" class="blockOfPlots">

    <div class="caption">JVM: heap, native memory, threads, CPU</div>

    <div id="jvmBasics_Heap_Chart" class="plot hideable"></div>
    <div id="jvmBasics_OffHeap_Chart" class="plot hideable"></div>
    <div id="jvmBasics_DirectByteBuffers_Chart" class="plot hideable"></div>
    <div id="jvmBasics_Threads_Chart" class="plot hideable"></div>

    <div id="jvmBasics_GC_Times_Chart" class="plot hideable"></div>
    <div id="jvmBasics_Allocations_Chart" class="plot hideable"></div>
    <div id="jvmBasics_JVM_CPUTime_Chart" class="plot hideable"></div>
    <div id="jvmBasics_OS_LoadAvg_Chart" class="plot hideable"></div>
  </div>

  <div id="AWT_and_Flush_Queues" class="blockOfPlots">

    <div class="caption">EDT: AWT & Flush Queues</div>

    <div id="EDT_awtEventsDispatchedChart" class="plot hideable"></div>
    <div id="EDT_awtEventsTimingsChart" class="plot hideable"></div>

    <!--<div class="caption">FlushQueue (tasks dispatching):</div>-->

    <div id="FlushQueue_tasksExecutedChart" class="plot hideable"></div>
    <div id="FlushQueue_tasksWaitingTimesChart" class="plot hideable"></div>
    <div id="FlushQueue_tasksExecutionTimesChart" class="plot hideable"></div>
  </div>

  <div id="Write_Read_Actions" class="blockOfPlots">

    <div class="caption">Actions: Write, Read, and Non-Blocking-Read</div>

    <div id="ReadAndWriteActions_CountChart" class="plot hideable"></div>

    <div id="NonBlockingReads_CountChart" class="plot hideable"></div>
    <div id="NonBlockingReads_TimesChart" class="plot hideable"></div>
  </div>

  <div id="indexes" class="blockOfPlots">

    <div class="caption">Indexes: lookups count & duration</div>

    <div id="indexes_Lookups_Chart" class="plot hideable"></div>
    <div id="indexes_Durations_Chart" class="plot hideable"></div>

  </div>

  <div id="filePageCache" class="blockOfPlots">

    <div class="caption">FilePageCache:</div>

    <div id="filePageCache_HitsMisses_Chart" class="plot hideable"></div>
    <div id="filePageCache_Times_Chart" class="plot hideable"></div>

    <div id="directBufferAllocator_Counts_Chart" class="plot hideable"></div>
    <div id="directBufferAllocator_Bytes_Chart" class="plot hideable"></div>


  </div>

  <div id="filePageCacheLockFree" class="blockOfPlots">

    <div class="caption">FilePageCache (New):</div>

    <div id="filePageCacheLockFree_PageCounts" class="plot hideable"></div>

    <div id="filePageCacheLockFree_PageTimings" class="plot hideable"></div>
    <div id="filePageCacheLockFree_PageBytes" class="plot hideable"></div>

    <div id="filePageCacheLockFree_Caching" class="plot hideable"></div>

    <div id="filePageCacheLockFree_HeapVsNativeMemoryUsed" class="plot hideable"></div>

    <div id="filePageCacheLockFree_Housekeeper" class="plot hideable"></div>
    <div id="filePageCacheLockFree_HousekeeperTimes" class="plot hideable"></div>

  </div>

  <div id="custom" class="blockOfPlots">
    <div class="caption">
      <label for="timeSeriesChooser">Plot other:</label>
      <select name="timeSeriesChooser" id="timeSeriesChooser"
              multiple
              onchange="extractAndPlotCustomTimeSeries($(this).val())">
      </select>
      (no more than 6 time series at once)
    </div>


    <div id="customChartPlotly" class="plot hideable"></div>
  </div>
</div>

<script lang="js">

  /* Make page accept drag-n-drop files */
  initDnD = function (dragAndDropAreaElement, processFiles) {
    //area 'sensitive' to drag-n-drop:
    const dragAndDropArea = $(dragAndDropAreaElement)

    //create 'glass pane' for DnD visual signalling:
    const dragAndDropGlassPane = $('<div/>', {
      id: 'dragAndDropGlassPane',
      style: "position:absolute; top:0;bottom:0;left:0;right:0; z-index:1000;pointer-events:none;"
    })
    dragAndDropGlassPane.appendTo(dragAndDropArea)
    dragAndDropGlassPane.hide()
    const dragAndDropAssistant = $("<div/>", {
      id: 'dragAndDropAssistant',
      style: 'position:absolute; z-index:1001; font-size: 2em; ' +
        'margin-top:-1.5em; margin-left:-5em; ' +
        'display:none; pointer-events:none;' +
        'color: rgb(0, 100, 0);'
    }).text("Drop it. Right here. Now.")
    dragAndDropGlassPane.append(dragAndDropAssistant)


    dragAndDropGlassPane.showPanel = (event, readyToAcceptDrop) => {
      if (readyToAcceptDrop) {
        dragAndDropGlassPane.css("background-color", "rgba(200, 220, 200, 0.7)")
      }
      else {
        dragAndDropGlassPane.css("background-color", "rgba(220, 200, 200, 0.7)")
      }
      if (event && readyToAcceptDrop) {
        //show validating text under cursor:
        const x = event.clientX + 10
        const y = event.clientY - 10
        dragAndDropAssistant.css({left: x - 10, top: y}).show()
      }
      dragAndDropGlassPane.show()
    }
    dragAndDropGlassPane.hidePanel = () => {
      dragAndDropGlassPane.hide()
    }

    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
      dragAndDropArea.on(eventName, (event) => {
        event.preventDefault()
        event.stopPropagation()

        //console.log(event.originalEvent)

        switch (event.type) {
          case "dragenter":
          case "dragover":
            dragAndDropGlassPane.showPanel(event, /*accept: */true)
            break
          case "dragleave":
            dragAndDropGlassPane.hidePanel()
            break
          case "drop":
            dragAndDropGlassPane.hidePanel()
            if (event.originalEvent.dataTransfer) {
              const dataTransfer = event.originalEvent.dataTransfer
              const files = dataTransfer.files
              if (files.length && files.length > 0) {
                processFiles(files)
              }
            }
            break
        }
      })
    })

  }
  initDnD(document.body, readFiles)

  initProgressBar = function () {
    const progressBarGlassPane = $("#progressBar")

    const textPane = $("#progressBar #caption")
    const percentsPane = $("#progressBar #percents")
    progressBarGlassPane.hide()

    let percentValue = 0
    document.progressBar = {
      show: (caption) => {
        textPane.text(caption)
        percentsPane.text(percentValue.toFixed(0))
        progressBarGlassPane.show()
      },
      update: (percents) => {
        percentValue = percents
        percentsPane.text(percents.toFixed(0))
      },
      hide: () => {
        progressBarGlassPane.hide()
      },
      currentValue: () => {
        return percentValue
      }
    }
  }
  initProgressBar()


  //setup panels close/open by clicking on the panel caption:
  $('.caption').on('click', (event) => {
    //ignore clicks propagated from elements _inside_ .caption -- those elements could
    // have their own use for mouse clicks, so don't interfere with them:
    if (event.currentTarget === event.target) {
      const parent = $(event.currentTarget.parentElement)
      parent.toggleClass('hidden')
    }
  })

</script>
</body>
</html>